package com.khstudio.videocapture.decoder;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.LinkedList;

import android.graphics.Bitmap;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.Message;
import android.view.Surface;

import com.khstudio.api.util.KhLog;

public class KhMediaDecoder {
    public static final int SEEK_TO_PREVIOUS_SYNC   = MediaExtractor.SEEK_TO_PREVIOUS_SYNC;
    public static final int SEEK_TO_NEXT_SYNC       = MediaExtractor.SEEK_TO_NEXT_SYNC;
    public static final int SEEK_TO_CLOSEST_SYNC    = MediaExtractor.SEEK_TO_CLOSEST_SYNC;
    public static final int SEEK_TO_ACCURACY        = MediaExtractor.SEEK_TO_CLOSEST_SYNC + 1;

    private static final int    NONE            = 0x0000;

    private int                 mState;
    private static final int    PREPARE         = 0x0001;
    private static final int    PLAY            = 0x0010;
    private static final int    SEEK            = 0x0020;
    private static final int    GET_FRAME       = 0x0040;
    private static final int    RELEASE         = 0x0080;
    private static final int    PLAY_CB         = 0x0100;
    private static final int    SEEK_CB         = 0x0200;
    private static final int    GET_FRAME_CB    = 0x0400;
    private static final int    RELEASE_CB      = 0x0800;

    private static final int    MSG_PREPARE_CB      = 0x0001;
    private static final int    MSG_SEEK_CB         = 0x0002;
    private static final int    MSG_PLAY_CB         = 0x0003;
    private static final int    MSG_GET_FRAME_CB    = 0x0004;
    private static final int    MSG_ERROR_CB        = 0x0005;
    private static final int    MSG_RELEASE_CB      = 0x0006;
    private static final int    MSG_START_THREAD    = 0x0007;

    private int                 vSurfaceConfigure;
    private static final int    SURFACE_HW          = 0x0001;
    private static final int    SURFACE_TEXTURE     = 0x0002;

    private KhMediaDecoder              mMD;
    private MediaThread                 mMT;
    private boolean                     mHasAudio;
    private boolean                     mHasVideo;
    private boolean                     mOnlyAudio;
    private boolean                     mOnlyVideo;
    private boolean                     mThreadRunning;

    private String                      mSource;

    private OnMediaDecoderPrepareListener     mPrepareListener;
    private OnMediaDecoderSeekListener        mSeekListener;
    private OnMediaDecoderPlayListener        mPlayListener;
    private OnMediaDecoderGetFrameListener    mGetFrameListener;
    private OnMediaDecoderErrorListener       mErrorListener;
    private OnMediaDecoderReleaseListener     mReleaseListener;

    private FileInputStream             aFi;
    private FileDescriptor              aFd;

    private MediaExtractor              aExtractor;
    private MediaCodec                  aDecoder;
    private MediaFormat                 aFormat;
    private String                      aMime;
    private AudioTrack                  aAudioTrack;

    private boolean                     aSawInputEOS;
    private boolean                     aSawOutputEOS;

    private ByteBuffer[]                aCodecInputBuffers;
    private ByteBuffer[]                aCodecOutputBuffers;

    private long                        aInputTimeUs;
    private long                        aOutputTimeUs;

    private FileInputStream             vFi;
    private FileDescriptor              vFd;

    private MediaExtractor              vExtractor;
    private MediaCodec                  vDecoder;
    private MediaFormat                 vFormat;
    private String                      vMime;

    private boolean                     vSawInputEOS;
    private boolean                     vSawOutputEOS;

    private ByteBuffer[]                vCodecInputBuffers;
    private ByteBuffer[]                vCodecOutputBuffers;

    private long                        vInputTimeUs;
    private long                        vOutputTimeUs;
    private ArrayList<Long>             vSyncTable;
    private LinkedList<Long>            vInputTimeList;

    private Surface                     vSurfaceHW;
    private KhMediaSurfaceTexture       vSurfaceTexture;

    private static final long           kTimeOutUs = 5000;
    private long                        mSeekTimeUs;
    private long                        mDuration;

    private long[]                      mGetFrameTimeUs;
    private int                         mGetFrameCnt;

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case MSG_PREPARE_CB : {
                    if(mPrepareListener != null) mPrepareListener.onPrepare(mMD);
                }
                break;
                case MSG_SEEK_CB : {
                    if(mSeekListener != null) mSeekListener.onSeek(mMD);
                }
                break;
                case MSG_PLAY_CB : {
                    if(mPlayListener != null) mPlayListener.onPlay(mMD, msg.arg1 > 0 ? true : false);
                }
                break;
                case MSG_GET_FRAME_CB : {
                    if(mGetFrameListener != null) mGetFrameListener.onGetFrame(mMD, (Bitmap) msg.obj, (msg.arg1 == 1) ? true : false);
                }
                break;
                case MSG_ERROR_CB : {
                    if(mErrorListener != null) mErrorListener.onError(mMD, msg.arg1, msg.arg2);
                }
                break;
                case MSG_RELEASE_CB : {
                    if(mReleaseListener != null) mReleaseListener.onRelease(mMD);
                }
                break;
                case MSG_START_THREAD : {
                    if(vSurfaceHW == null) {
                        mHandler.sendEmptyMessageDelayed(MSG_START_THREAD, 10);
                    } else if(!mThreadRunning) {
                        mThreadRunning = true;
                        mMT = new MediaThread();
                        mMT.start();
                    }
                }
                break;
            }
        }
    };

    public interface OnMediaDecoderPrepareListener {
        void onPrepare(KhMediaDecoder md);
    }

    public interface OnMediaDecoderPlayListener {
        void onPlay(KhMediaDecoder md, boolean eos);
    }

    public interface OnMediaDecoderSeekListener {
        void onSeek(KhMediaDecoder md);
    }

    public interface OnMediaDecoderGetFrameListener {
        void onGetFrame(KhMediaDecoder md, Bitmap frame, boolean isLast);
    }

    public interface OnMediaDecoderErrorListener {
        void onError(KhMediaDecoder md, int what, int extra);
    }

    public interface OnMediaDecoderReleaseListener {
        void onRelease(KhMediaDecoder md);
    }

    public KhMediaDecoder() {
        mMD             = this;
        mMT             = null;

        mState          = NONE;
        mThreadRunning  = false;

        mHasAudio       = false;
        mHasVideo       = false;
        mOnlyAudio      = false;
        mOnlyVideo      = false;

        mSource         = null;

        aFi                 = null;
        aFd                 = null;
        aExtractor          = null;
        aDecoder            = null;
        aFormat             = null;
        aMime               = null;
        aAudioTrack         = null;
        aSawInputEOS        = true;
        aSawOutputEOS       = true;
        aCodecInputBuffers  = null;
        aCodecOutputBuffers = null;

        aInputTimeUs        = 0;
        aOutputTimeUs       = 0;

        vFi                 = null;
        vFd                 = null;
        vExtractor          = null;
        vDecoder            = null;
        vFormat             = null;
        vMime               = null;
        vSawInputEOS        = true;
        vSawOutputEOS       = true;
        vCodecInputBuffers  = null;
        vCodecOutputBuffers = null;

        vSyncTable          = null;
        vInputTimeList      = null;
        vInputTimeUs        = -1;
        vOutputTimeUs       = -1;

        vSurfaceConfigure   = NONE;
        vSurfaceHW          = null;
        vSurfaceTexture     = null;

        mDuration       = 0;
        mSeekTimeUs     = 0;

        mGetFrameTimeUs     = null;
        mGetFrameCnt        = 0;
    }

    public void setOnSeekListener(OnMediaDecoderSeekListener l) {
        mSeekListener = l;
    }

    public void setOnPlayListener(OnMediaDecoderPlayListener l) {
        mPlayListener = l;
    }

    public void setOnGetFrameListener(OnMediaDecoderGetFrameListener l) {
        mGetFrameListener = l;
    }

    public void setOnErrorListener(OnMediaDecoderErrorListener l) {
        mErrorListener = l;
    }

    public void setOnReleaseListener(OnMediaDecoderReleaseListener l) {
        mReleaseListener = l;
    }

    public void setDataSource(String s) {
        mSource = s;
    }

    public String getDataSource() {
        return mSource;
    }

    public long getDurationUs() {
        return mDuration;
    }

    public long getCurrnetTimeUs() {
        return vOutputTimeUs;
    }

    public MediaFormat getVideoFormat() {
        if(mHasVideo) {
            return vFormat;
        }
        return null;
    }

    public void setSurface(Surface surface) {
        vSurfaceHW = surface;
    }

    public boolean isPlay() {
        if((mState & PLAY) == PLAY) {
            return true;
        }
        return false;
    }

    public void playOnlyVideoTrack() {
        mOnlyAudio = false;
        mOnlyVideo = true;
    }

    public void playOnlyAudioTrack() {
        mOnlyAudio = true;
        mOnlyVideo = false;
    }

    public void prepare(OnMediaDecoderPrepareListener l) {
        if((mState & PREPARE) != PREPARE) {
            if(l == null) {
                _prepare();
            } else {
                mPrepareListener = l;

                if(!mThreadRunning) {
                    mThreadRunning = true;
                    mMT = new MediaThread();
                    mMT.start();
                }
            }
        } else {
            if(l == null) l.onPrepare(mMD);
        }
    }

    public void play() {
        KhLog.i("KhMediaDecoder : play()");
        mState |= (PLAY | PLAY_CB);

        if(vSurfaceHW == null) {
            mHandler.sendEmptyMessageDelayed(MSG_START_THREAD, 10);
        } else if(!mThreadRunning) {
            mThreadRunning = true;
            mMT = new MediaThread();
            mMT.start();
        }
    }

    public void seek(long timeUs) {
        KhLog.i("KhMediaDecoder : seek() timeUS = %d", timeUs);
        mSeekTimeUs = timeUs;
        mState |= SEEK;

        if(vSurfaceHW == null) {
            mHandler.sendEmptyMessageDelayed(MSG_START_THREAD, 10);
        } else if(!mThreadRunning) {
            mThreadRunning = true;
            mMT = new MediaThread();
            mMT.start();
        }
    }

    public void getFrame(long timeUs, int mode) {
        long[] timeUsArray = new long[1];

        timeUsArray[0] = _getSyncTimeUs(timeUs, mode);

        _getFrame(timeUsArray);
    }

    public void getFrameList(long sTimeUs, long eTimeUs, int tCnt, boolean sync) {
        long[] timeUsArray = new long[tCnt];

        for (int i = 0; i < tCnt; i++) {
            long sTimetmpUs = (long) (sTimeUs + (eTimeUs - sTimeUs) * ((double) (i) / (double) tCnt));
            long eTimetmpUs = (long) (sTimeUs + (eTimeUs - sTimeUs) * ((double) (i + 1) / (double) tCnt));

            long mTimetmpUs = sTimetmpUs + (eTimetmpUs - sTimetmpUs) / 2;

            if(sync) {
                long closeSyncTimeUs = _getSyncTimeUs(mTimetmpUs, SEEK_TO_CLOSEST_SYNC);
                if(closeSyncTimeUs > sTimetmpUs && closeSyncTimeUs < eTimetmpUs) {
                    timeUsArray[i] = closeSyncTimeUs;
                } else {
                    timeUsArray[i] = mTimetmpUs;
                }
            } else {
                timeUsArray[i] = mTimetmpUs;
            }
        }

        _getFrame(timeUsArray);
    }

    public void stop() {
        KhLog.i("KhMediaDecoder : stop()");
        mState &= ~PLAY;
    }

    public void release() {
        KhLog.i("KhMediaDecoder : release()");
        mState |= (RELEASE | RELEASE_CB);
        if(!mThreadRunning) _release();
    }

    private boolean _prepare() {
        mState |= PREPARE;

        mHasAudio = false;
        mHasVideo = false;

        KhLog.i("KhMediaDecoder : _prepare Path = %s", mSource);
        if(mSource == null) {
            KhLog.e("KhMediaDecoder : _prepare Path is null");
            return false;
        }

        File srcFile = new File(mSource);
        if(!srcFile.exists()) {
            KhLog.e("KhMediaDecoder : _prepare File is not exist");
            return false;
        }

        if(!mOnlyVideo) {
            mHasAudio = _prepareAudio(srcFile);
        }

        if(!mOnlyAudio) {
            mHasVideo = _prepareVideo(srcFile);
        }

        return (mHasAudio || mHasVideo);
    }

    private boolean _prepareAudio(File file) {
        try {
            if (aFi != null) {
                aFi.close();
            }
            aFi = new FileInputStream(file);
            aFd = aFi.getFD();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }

        if(aExtractor != null) {
            aExtractor.release();
        }
        aExtractor = new MediaExtractor();
        try {
            aExtractor.setDataSource(aFd, 0, file.length());
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }

        int track = _findAudioTrackNumber(aExtractor);

        if(track != -1) {
            aFormat = aExtractor.getTrackFormat(track);
            aMime   = aFormat.getString(MediaFormat.KEY_MIME);

            aDecoder = MediaCodec.createDecoderByType(aMime);
            aDecoder.configure(aFormat, null, null /* crypto */, 0 /* flags */);
            aDecoder.start();
            aExtractor.selectTrack(track);

            aCodecInputBuffers = aDecoder.getInputBuffers();
            aCodecOutputBuffers = aDecoder.getOutputBuffers();

            aOutputTimeUs = 0;
            aInputTimeUs = 0;

            aSawInputEOS = false;
            aSawOutputEOS = false;

            mHasAudio = true;
        } else {
            KhLog.e("KhMediaDecoder : _prepareAudio Not found audio track");
            return false;
        }

        KhLog.i("KhMediaDecoder : _prepareAudio prepare audio");
        return true;
    }

    private boolean _prepareVideo(File file) {
        try {
            if(vFi != null) {
                vFi.close();
            }
            vFi = new FileInputStream(file);
            vFd = vFi.getFD();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }

        if(vExtractor != null) {
            vExtractor.release();
        }
        vExtractor = new MediaExtractor();
        try {
            vExtractor.setDataSource(vFd, 0, file.length());
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }

        int track = _findVideoTrackNumber(vExtractor);

        if(track != -1) {
            vFormat = vExtractor.getTrackFormat(track);
            vMime   = vFormat.getString(MediaFormat.KEY_MIME);

            vDecoder = MediaCodec.createDecoderByType(vMime);
            vExtractor.selectTrack(track);

            vOutputTimeUs = -1;
            vInputTimeUs = -1;

            vSawInputEOS = false;
            vSawOutputEOS = false;

            vInputTimeList = new LinkedList<Long>();

            _initVSyncSampleTable();
        } else {
            KhLog.e("KhMediaDecoder : _prepareVideo Not found video track");
            return false;
        }

        KhLog.i("KhMediaDecoder : _prepareVideo prepare video");
        return true;
    }

    private void _release() {
        KhLog.i("KhMediaDecoder : _release()");
        if (aAudioTrack != null) {
            aAudioTrack.flush();
            aAudioTrack.stop();
            aAudioTrack.release();
            aAudioTrack = null;
        }

        if(aDecoder != null) {
            try {
                aDecoder.flush();
            } catch(Exception e) {
                e.printStackTrace();
            }
            aDecoder.stop();
            aDecoder.release();
            aDecoder = null;
        }

        if(aExtractor != null) {
            aExtractor.release();
            aExtractor = null;
        }

        try {
            if(aFi != null) {
                aFi.close();
                aFi = null;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        if(vSyncTable != null) {
            vSyncTable.clear();
            vSyncTable = null;
        }

        if(vInputTimeList != null) {
            vInputTimeList.clear();
            vInputTimeList = null;
        }

        if(vDecoder != null) {
            vDecoder.flush();
            vDecoder.stop();
            vDecoder.release();
            vDecoder = null;
        }

        if(vExtractor != null) {
            vExtractor.release();
            vExtractor = null;
        }

        if(vSurfaceTexture != null) {
            vSurfaceTexture.release();
            vSurfaceTexture = null;
        }

        try {
            if(vFi != null) {
                vFi.close();
                vFi = null;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        vSurfaceConfigure = NONE;

        if((mState & RELEASE_CB) == RELEASE_CB) _onRelease();
        mState &= ~(PREPARE | PLAY | PLAY_CB | SEEK | SEEK_CB | RELEASE | RELEASE_CB);
    }

    private void _getFrame(long[] timeUs) {
        mGetFrameTimeUs = timeUs;
        mGetFrameCnt = 0;
        mState |= (GET_FRAME | GET_FRAME_CB);

        if(!mThreadRunning) {
            mThreadRunning = true;
            mMT = new MediaThread();
            mMT.start();
        }
    }

    private int _findVideoTrackNumber(MediaExtractor extractor) {
        for(int i = 0; i < extractor.getTrackCount(); i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            KhLog.i("KhMediaDecoder : _findVideoTrackNumber i = %d mime = %s",i,mime);

            if (mime.toLowerCase().startsWith("video")) {
                return i;
            }
        }
        return -1;
    }

    private int _findAudioTrackNumber(MediaExtractor extractor) {
        for(int i = 0; i < extractor.getTrackCount(); i++) {
            MediaFormat format = extractor.getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            KhLog.i("KhMediaDecoder : _findAudioTrackNumber i = %d mime = %s",i,mime);

            if (mime.toLowerCase().startsWith("audio")) {
                return i;
            }
        }
        return -1;
    }

    private void _onPrepare() {
        mHandler.sendEmptyMessage(MSG_PREPARE_CB);
    }

    private void _onSeek() {
        mHandler.sendEmptyMessage(MSG_SEEK_CB);
    }

    private void _onPlay(boolean eos) {
        Message msg = mHandler.obtainMessage();
        msg.what = MSG_PLAY_CB;
        msg.arg1 = eos ? 1 : 0;
        mHandler.sendMessage(msg);
    }

    private void _onGetFrame(Bitmap frame, boolean isLast) {
        Message msg = mHandler.obtainMessage();
        msg.what = MSG_GET_FRAME_CB;
        msg.arg1 = isLast ? 1 : 0;
        msg.obj = frame;
        mHandler.sendMessage(msg);
    }

    private void _onError(int what, int extra) {
        Message msg = mHandler.obtainMessage();
        msg.what = MSG_ERROR_CB;
        msg.arg1 = what;
        msg.arg2 = extra;
        mHandler.sendMessage(msg);
    }

    private void _onRelease() {
        mHandler.sendEmptyMessage(MSG_RELEASE_CB);
    }

    private void _initVSyncSampleTable() {
        if(vSyncTable != null) {
            return;
        }
        vSyncTable = new ArrayList<Long>();

        boolean SawEos = false;
        vExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
        long tmpTime = vExtractor.getSampleTime();
        long duration = tmpTime;
        int advanceCnt = 1;

        vSyncTable.add(Long.valueOf(duration)); /* First Sync Frame */
        while(!SawEos) {
            for(int i = 0 ; i < advanceCnt ; i++) {
                vExtractor.advance();
            }
            tmpTime = vExtractor.getSampleTime();

            vExtractor.seekTo(tmpTime, MediaExtractor.SEEK_TO_NEXT_SYNC);
            tmpTime = vExtractor.getSampleTime();
            if(tmpTime == 0) {
                advanceCnt++;
                continue;
            }

            if(tmpTime < 0) {
                SawEos = true;
            } else {
                duration = tmpTime;
                vSyncTable.add(Long.valueOf(duration));
            }
        }

        SawEos = false;
        vExtractor.seekTo(duration, MediaExtractor.SEEK_TO_NEXT_SYNC);
        while(!SawEos) {
            vExtractor.advance();
            tmpTime = vExtractor.getSampleTime();
            if(tmpTime < 0) {
                SawEos = true;
            } else {
                duration = tmpTime;
            }
        }

        vSyncTable.add(Long.valueOf(Long.MAX_VALUE)); /* Last */

        vExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
        mDuration = duration;
    }

    private int _arrayListPrevPos(ArrayList<Long> al, long timeUs) {
        int nScopeMax = al.size() - 1;
        int nScopeMin = 0;
        int nIdx = nScopeMax / 2;
        int prevPos = -1;

        if(timeUs > al.get(nScopeMax).longValue()) {
            return nScopeMax;
        }

        while(true) {
            long tmpTime = al.get(nIdx).longValue();
            if(tmpTime == timeUs) {
                prevPos = nIdx;
                break;
            } else if(timeUs < tmpTime) {
                nScopeMax = nIdx;
                nIdx = (nScopeMax+nScopeMin) / 2;
                if(nIdx == nScopeMin) {
                    prevPos = nScopeMin;
                    break;
                }
            } else {
                nScopeMin = nIdx;
                nIdx = (nScopeMax+nScopeMin) / 2;
                if(nIdx == nScopeMin) {
                    prevPos = nScopeMin;
                    break;
                }
            }
        }

        return prevPos;
    }

    private long _getSyncTimeUs(long timeUs, int mode) {
        int nIdx = 0;
        long syncTimeUs = 0;

        if(mode == SEEK_TO_ACCURACY) {
            return timeUs;
        }

        nIdx = _arrayListPrevPos(vSyncTable, timeUs);

        if(mode == SEEK_TO_PREVIOUS_SYNC) {
            syncTimeUs = vSyncTable.get(nIdx).longValue();
        } else if(mode == SEEK_TO_NEXT_SYNC) {
            syncTimeUs = vSyncTable.get(nIdx+1).longValue();
        } else { /* SEEK_TO_CLOSEST_SYNC */
            long prevUs = vSyncTable.get(nIdx).longValue();
            long nextUs = vSyncTable.get(nIdx+1).longValue();
            if(timeUs - prevUs < nextUs - timeUs) {
                syncTimeUs = prevUs;
            } else {
                syncTimeUs = nextUs;
            }
        }
        return syncTimeUs;
    }

    private class MediaThread extends Thread {
        @Override
        public void run() {
            while(true) {
                if((mState & PREPARE) != PREPARE) {
                    if(_prepare()) {
                        _onPrepare();
                    } else {
                        _release();
                        mState = NONE;
                        _onError(0, 0);
                        break;
                    }
                }

                if((mState & RELEASE) == RELEASE) {
                    _release();
                    break;
                } else if((mState & GET_FRAME) == GET_FRAME) {
                    Bitmap frame = _getframe(mGetFrameTimeUs[mGetFrameCnt]);
                    if(frame != null) {
                        if(++mGetFrameCnt < mGetFrameTimeUs.length) {
                            _onGetFrame(frame, false);
                        } else {
                            mState &= ~GET_FRAME;
                            mGetFrameTimeUs = null;
                            mGetFrameCnt = 0;
                            _onGetFrame(frame, true);
                        }
                    } else {
                        mState &= ~GET_FRAME;
                        mGetFrameTimeUs = null;
                        mGetFrameCnt = 0;
                    }
                } else if((mState & SEEK) == SEEK) {
                    _seeking();
                    if((mState & SEEK) == 0) {
                        _onSeek();
                    }
                } else if((mState & PLAY) == PLAY) {
                    _playing();
                    if(aSawOutputEOS && vSawOutputEOS) {
                        mState &= ~PLAY;
                        _onPlay(true);
                    } else if((mState & PLAY) == 0) {
                        _onPlay(false);
                    }
                } else {
                    break;
                }
            }

            mThreadRunning = false;
        }

        private void _playing() {
            int noVideoOutputCounter = 0;
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

            long    baseTimeMS = 0;
            boolean audioStart = false;

            boolean aPresentationOutput = false;
            boolean vPresentationOutput = false;

            int  aAudioBufferSize = 0;
            long aAudioBufferPosition = 0;
            long aAudioPlayBackPositionPrev = 0;
            int  aAudioBufferCoeffieient = 0;

            if(mHasAudio) {
                int channel_count = aFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
                if(channel_count == 1) {
                    channelConfig = AudioFormat.CHANNEL_OUT_MONO;
                }

                aAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                        aFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE),
                        channelConfig,
                        AudioFormat.ENCODING_DEFAULT,
                        aCodecOutputBuffers[0].capacity(),
                        AudioTrack.MODE_STREAM);

                if(aAudioTrack.getAudioFormat() == AudioFormat.ENCODING_PCM_16BIT) {
                    aAudioBufferCoeffieient = channel_count * 2;
                } else {
                    aAudioBufferCoeffieient = channel_count;
                }
                aAudioBufferSize = aCodecOutputBuffers[0].capacity()/aAudioBufferCoeffieient;

                if (aAudioTrack == null || aAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
                    KhLog.e("KhMediaDecoder : _playing error AudioTrack");
                    return;
                }
                aAudioTrack.play();

                baseTimeMS = aExtractor.getSampleTime() / 1000;
            } else if(mHasVideo) {
                if(vInputTimeList.size() > 0) {
                    baseTimeMS = System.currentTimeMillis() - vInputTimeList.element() / 1000;
                } else {
                    baseTimeMS = System.currentTimeMillis() - vExtractor.getSampleTime() / 1000;
                }
            }

            if(mHasVideo) {
                _surfaceConfigure(SURFACE_HW);
            }

            while (!vSawOutputEOS || !aSawOutputEOS) {
                if(noVideoOutputCounter > 50) {
                    KhLog.w("KhMediaDecoder : _playing noOutputCounter over 50");
                    break;
                }

                if((mState & SEEK) == SEEK || (mState & RELEASE) == RELEASE || (mState & PLAY) != PLAY) {
                    KhLog.i("KhMediaDecoder : _playing state change = 0x%x", mState);
                    break;
                }

                /* Audio Play */
                if(mHasAudio && !aSawOutputEOS) {
                    if(!aPresentationOutput) {
                        long headPos = aAudioTrack.getPlaybackHeadPosition();
                        if(headPos < aAudioPlayBackPositionPrev) {
                            if((aAudioBufferPosition & 0x80000000) == 0x80000000) {
                                aAudioBufferPosition &= 0x7FFFFFFF;
                            } else {
                                headPos += 0x7FFFFFFF;
                            }
                        } else {
                            aAudioPlayBackPositionPrev = headPos;
                        }
                        headPos += aAudioBufferSize;

                        if(headPos > aAudioBufferPosition) {
                            aPresentationOutput = true;
                        }
                    }

                    if (!aSawInputEOS) {
                        int inputBufIndex = aDecoder.dequeueInputBuffer(kTimeOutUs);

                        if (inputBufIndex >= 0) {
                            ByteBuffer dstBuf = aCodecInputBuffers[inputBufIndex];

                            int sampleSize = aExtractor.readSampleData(dstBuf, 0 /* offset */);

                            if (sampleSize < 0) {
                                KhLog.i("KhMediaDecoder : _playing Audio saw input EOS.");
                                aSawInputEOS = true;
                                sampleSize = 0;
                            } else {
                                aInputTimeUs = aExtractor.getSampleTime();
                            }

                            aDecoder.queueInputBuffer(
                                    inputBufIndex,
                                    0 /* offset */,
                                    sampleSize,
                                    aInputTimeUs,
                                    aSawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);

                            if (!aSawInputEOS) {
                                aExtractor.advance();
                            }
                        }
                    }

                    if(aPresentationOutput) {
                        int res = aDecoder.dequeueOutputBuffer(info, kTimeOutUs);

                        if (res >= 0) {
                            int outputBufIndex = res;
                            aOutputTimeUs = info.presentationTimeUs;

                            byte[] pcm = new byte[info.size];
                            aCodecOutputBuffers[outputBufIndex].get(pcm, 0, info.size);
                            aCodecOutputBuffers[outputBufIndex].clear();

                            aAudioBufferPosition += (info.size / aAudioBufferCoeffieient);

                            aAudioTrack.write(pcm, 0, info.size);

                            aDecoder.releaseOutputBuffer(outputBufIndex, false /* render */);

                            aPresentationOutput = false;

                            if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                                KhLog.i("KhMediaDecoder : _playing Audio saw output EOS.");
                                aSawOutputEOS = true;
                            }
                        } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                            aCodecOutputBuffers = aDecoder.getOutputBuffers();
                            KhLog.v("KhMediaDecoder : _playing Audio output buffers have changed.");
                        } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                            MediaFormat oformat = aDecoder.getOutputFormat();
                            KhLog.v("KhMediaDecoder : _playing Audio output format has changed to " + oformat);
                        }
                    }
                }

                if(mHasVideo && !vSawOutputEOS) {
                    if(!vPresentationOutput) {
                        if(mHasAudio) {
                            if(!audioStart) {
                                long audioTime = 1000 *  aAudioTrack.getPlaybackHeadPosition() / aAudioTrack.getPlaybackRate();
                                if(audioTime > 0) {
                                    audioStart = true;
                                    baseTimeMS = System.currentTimeMillis() - baseTimeMS - audioTime;
                                }
                            }

                            if(audioStart) {
                                if((_nextPresentationTimeUs() / 1000) <= System.currentTimeMillis() - baseTimeMS) {
                                    vPresentationOutput = true;
                                }
                            }
                        } else {
                            if(baseTimeMS + vOutputTimeUs / 1000 <= System.currentTimeMillis()) {
                                vPresentationOutput = true;
                            }
                        }
                    }

                    if (!vSawInputEOS) {
                        int inputBufIndex = vDecoder.dequeueInputBuffer(kTimeOutUs);

                        if (inputBufIndex >= 0) {
                            ByteBuffer dstBuf = vCodecInputBuffers[inputBufIndex];

                            int sampleSize = vExtractor.readSampleData(dstBuf, 0 /* offset */);

                            if (sampleSize < 0) {
                                KhLog.i("KhMediaDecoder : _playing Video saw input EOS.");
                                vSawInputEOS = true;
                                sampleSize = 0;
                            } else {
                                vInputTimeUs = vExtractor.getSampleTime();
                                vInputTimeList.offer(Long.valueOf(vInputTimeUs));
                            }

                            vDecoder.queueInputBuffer(
                                    inputBufIndex,
                                    0 /* offset */,
                                    sampleSize,
                                    vInputTimeUs,
                                    vSawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);

                            if (!vSawInputEOS) {
                                vExtractor.advance();
                            }
                        }
                    }

                    if(vPresentationOutput) {
                        noVideoOutputCounter++;

                        int res = vDecoder.dequeueOutputBuffer(info, kTimeOutUs);

                        if (res >= 0) {
                            if (info.size > 0) {
                                noVideoOutputCounter = 0;
                            }

                            int outputBufIndex = res;
                            vOutputTimeUs = info.presentationTimeUs;
                            vInputTimeList.poll();

                            vDecoder.releaseOutputBuffer(outputBufIndex, true /* render */);

                            vPresentationOutput = false;
                            if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                                KhLog.i("KhMediaDecoder : _playing Video saw output EOS.");
                                vSawOutputEOS = true;
                            }
                        } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                            vCodecOutputBuffers = vDecoder.getOutputBuffers();
                            KhLog.v("KhMediaDecoder : _playing Video output buffers have changed.");
                        } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                            MediaFormat oformat = vDecoder.getOutputFormat();
                            KhLog.v("KhMediaDecoder : _playing Video output format has changed to " + oformat);
                        }
                    }
                }
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }

            if(mHasAudio) {
                if (aAudioTrack != null) {
                    aAudioTrack.flush();
                    aAudioTrack.stop();
                    aAudioTrack.release();
                    aAudioTrack = null;
                }
            }
        }

        private void _seeking() {
            mState &= ~SEEK;

            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

            long cSeekTimeUS = mSeekTimeUs;
            long savedPrevTimeUs = vOutputTimeUs;

            if(mHasVideo) {
                _surfaceConfigure(SURFACE_HW);

                vSawInputEOS = false;
                vSawOutputEOS = false;

                int prevSyncPos = _arrayListPrevPos(vSyncTable, cSeekTimeUS);
                long prevSyncTime = vSyncTable.get(prevSyncPos).longValue();
                long nextSyncTime = vSyncTable.get((prevSyncPos+1)).longValue();

                _seekExtractor(cSeekTimeUS);

                while(true) {

                    if (!vSawInputEOS) {
                        int inputBufIndex = vDecoder.dequeueInputBuffer(kTimeOutUs);
                        KhLog.d("KhMediaDecoder : _seeking inputBufIndex %d", vCodecOutputBuffers.length);
                        if (inputBufIndex >= 0) {
                            ByteBuffer dstBuf = vCodecInputBuffers[inputBufIndex];

                            int sampleSize = vExtractor.readSampleData(dstBuf, 0 /* offset */);

                            if (sampleSize < 0) {
                                KhLog.i("KhMediaDecoder : _seek saw input EOS.");
                                vSawInputEOS = true;
                                sampleSize = 0;
                            } else {
                                vInputTimeUs = vExtractor.getSampleTime();
                                vInputTimeList.offer(Long.valueOf(vInputTimeUs));
                                vExtractor.advance();
                            }

                            vDecoder.queueInputBuffer(
                                    inputBufIndex,
                                    0 /* offset */,
                                    sampleSize,
                                    vInputTimeUs,
                                    vSawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
                        }
                    }

                    int res = vDecoder.dequeueOutputBuffer(info, kTimeOutUs);

                    if (res >= 0) {
                        int outputBufIndex = res;
                        vOutputTimeUs = info.presentationTimeUs;
                        KhLog.i("KhMediaDecoder : _seek vOutputTimeUs = %d",vOutputTimeUs);

                        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                            KhLog.i("KhMediaDecoder : _seek saw output EOS.");
                            vSawOutputEOS = true;
                        }
                        vInputTimeList.poll();

                        KhLog.d("KhMediaDecoder : _seek vInputTimeList = " +vInputTimeList);
                        if(vSawOutputEOS
                            || vOutputTimeUs >= cSeekTimeUS
                            || _nextPresentationTimeUs() >= cSeekTimeUS
                            || _isNextSeek(cSeekTimeUS, savedPrevTimeUs, prevSyncTime, nextSyncTime)
                            || (mState & RELEASE) == RELEASE) {
                            vDecoder.releaseOutputBuffer(outputBufIndex, true /* render */);
                            KhLog.i("KhMediaDecoder : _seek render = %d", info.presentationTimeUs);
                            break;
                        } else {
                            vDecoder.releaseOutputBuffer(outputBufIndex, false /* render */);
                        }
                    } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                        vCodecOutputBuffers = vDecoder.getOutputBuffers();

                    } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        MediaFormat oformat = vDecoder.getOutputFormat();
                    }
                }
            }

            if(mHasAudio && (mState & SEEK) != SEEK) {
                aExtractor.seekTo(cSeekTimeUS, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
                aOutputTimeUs = aExtractor.getSampleTime();
                try {
                    aDecoder.flush();
                } catch(Exception e) {
                    e.printStackTrace();
                }
            }
        }

        private Bitmap _getframe(final long timeUs) {
            MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

            if(mHasVideo) {
                _surfaceConfigure(SURFACE_TEXTURE);

                vSawInputEOS = false;
                vSawOutputEOS = false;

                _seekExtractor(timeUs);

                while(!vSawOutputEOS) {
                    if (!vSawInputEOS) {
                        int inputBufIndex = vDecoder.dequeueInputBuffer(kTimeOutUs);

                        if (inputBufIndex >= 0) {
                            ByteBuffer dstBuf = vCodecInputBuffers[inputBufIndex];

                            int sampleSize = vExtractor.readSampleData(dstBuf, 0 /* offset */);

                            if (sampleSize < 0) {
                                vSawInputEOS = true;
                                sampleSize = 0;
                            } else {
                                vInputTimeUs = vExtractor.getSampleTime();
                                vInputTimeList.offer(Long.valueOf(vInputTimeUs));
                                vExtractor.advance();
                            }

                            vDecoder.queueInputBuffer(
                                    inputBufIndex,
                                    0 /* offset */,
                                    sampleSize,
                                    vInputTimeUs,
                                    vSawInputEOS ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
                        }
                    }

                    int res = vDecoder.dequeueOutputBuffer(info, kTimeOutUs);

                    if (res >= 0) {
                        int outputBufIndex = res;
                        vOutputTimeUs = info.presentationTimeUs;
                        vInputTimeList.poll();

                        if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                            vSawOutputEOS = true;
                        }

                        if(vSawOutputEOS
                            || _nextPresentationTimeUs() >= timeUs
                            || (mState & RELEASE) == RELEASE) {
                            vSurfaceTexture.makeCurrent();
                            KhLog.e("getFrameList frame reder true %d",timeUs);
                            vDecoder.releaseOutputBuffer(outputBufIndex, true /* render */);

                            vSurfaceTexture.awaitNewImage();
                            vSurfaceTexture.drawImage();
                            return vSurfaceTexture.getFrame();
                        } else {
                            KhLog.w("getFrameList frame reder false %d",vOutputTimeUs);
                            vDecoder.releaseOutputBuffer(outputBufIndex, false /* render */);
                        }
                    } else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                        vCodecOutputBuffers = vDecoder.getOutputBuffers();
                    } else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                        MediaFormat oformat = vDecoder.getOutputFormat();
                    }
                }
            }

            return null;
        }

        private void _seekExtractor(long timeUs) {
            if(vOutputTimeUs < timeUs) { /* Positive direction */
                if(timeUs < _getSyncTimeUs(vInputTimeUs, SEEK_TO_NEXT_SYNC)) {
                    KhLog.d("KhMediaDecoder : _seek Positive vInputTimeUs = %d",vExtractor.getSampleTime());
                } else {
                    vExtractor.seekTo(_getSyncTimeUs(timeUs, SEEK_TO_PREVIOUS_SYNC), MediaExtractor.SEEK_TO_CLOSEST_SYNC);
                    vDecoder.flush();
                    vInputTimeList.clear();
                    KhLog.d("KhMediaDecoder : _seek Positive seek to vInputTimeUs = %d",vExtractor.getSampleTime());
                }
            } else { /* negative & same */
                vExtractor.seekTo(_getSyncTimeUs(timeUs, SEEK_TO_PREVIOUS_SYNC), MediaExtractor.SEEK_TO_CLOSEST_SYNC);
                vDecoder.flush();
                vInputTimeList.clear();
                KhLog.d("KhMediaDecoder : _seek Negative seek to vInputTimeUs = %d",vExtractor.getSampleTime());
            }
        }

        private void _surfaceConfigure(int configure) {
            if(vSawOutputEOS || vSurfaceConfigure != configure) {
                vDecoder.stop();
                if(configure == SURFACE_HW) {
                    vDecoder.configure(vFormat, vSurfaceHW, null /* crypto */, 0 /* flags */);
                } else if(configure == SURFACE_TEXTURE) {
                    if(vSurfaceTexture != null) {
                        vSurfaceTexture.release();
                    }
                    vSurfaceTexture = new KhMediaSurfaceTexture(vFormat.getInteger(MediaFormat.KEY_WIDTH), vFormat.getInteger(MediaFormat.KEY_HEIGHT));
                    vDecoder.configure(vFormat, vSurfaceTexture.getSurface(), null /* crypto */, 0 /* flags */);
                }
                vDecoder.start();

                vCodecInputBuffers = vDecoder.getInputBuffers();
                vCodecOutputBuffers = vDecoder.getOutputBuffers();

                vSurfaceConfigure = configure;
            }
        }

        private boolean _isNextSeek(long cSeekTimeUs, long savedPrevTimeUs, long prevSyncTime, long nextSyncTime) {
            if((mState & SEEK) == SEEK) {
                long nSeekTimeUS = mSeekTimeUs;
                if(vOutputTimeUs < prevSyncTime || vOutputTimeUs > nextSyncTime) {
                    return false;
                }

                if(cSeekTimeUs < nSeekTimeUS) { /* Positive */
                    if(cSeekTimeUs >= savedPrevTimeUs) {
                        KhLog.d("KhMediaDecoder : _isNextSeek Positive current = %d , next = %d", cSeekTimeUs, nSeekTimeUS);
                        return true;
                    }
                } else if(cSeekTimeUs > nSeekTimeUS) {
                    if(vOutputTimeUs > nSeekTimeUS){ /* Negative */
                        KhLog.d("KhMediaDecoder : _isNextSeek Negative current = %d , next = %d", cSeekTimeUs, nSeekTimeUS);
                        return true;
                    }
                }
            }

            return false;
        }

        private long _nextPresentationTimeUs() {
            if(vInputTimeList.size() > 0) {
                KhLog.i("KhMediaDecoder : _seek 1 _nextPresentationTimeUs() %d", vInputTimeList.element());
                return vInputTimeList.element();
            } else {
                KhLog.i("KhMediaDecoder : _seek 2 _nextPresentationTimeUs() %d", vExtractor.getSampleTime());
                return vExtractor.getSampleTime();
            }
        }
    }
}
